namespace ODataValidator.RuleEngine.Common
{
    #region Namespaces
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using Newtonsoft.Json;
    using Newtonsoft.Json.Linq;
    #endregion

    /// <summary>
    /// Helper class of Json payload manipulation 
    /// </summary>
    public static class JsonHelper
    {
        /// <summary>
        /// Extension method to convert a string to JSON JObject object
        /// </summary>
        /// <param name="payload">payload content</param>
        /// <param name="jo">output parameter of the JObject object converted from payload literal</param>
        /// <returns>true if payload is converted to a JObject; otherwise false</returns>
        public static bool TryToJObject(this string payload, out JObject jo)
        {
            if (!string.IsNullOrEmpty(payload))
            {
                try
                {
                    jo = JObject.Parse(payload);
                    return true;
                }
                catch (Exception ex)
                {
                    if (!ExceptionHelper.IsCatchableExceptionType(ex))
                    {
                        throw;
                    }
                    // it's alright if JSON parsing complaints
                }
            }

            jo = null;
            return false;
        }

        /// <summary>
        /// Extension method to determine whether content is a valid Json literral
        /// </summary>
        /// <param name="payload">payload content</param>
        /// <returns>true/false</returns>
        public static bool IsJsonPayload(this string payload)
        {
            JObject jo;
            return payload.TryToJObject(out jo);
        }

        /// <summary>
        /// Extension method to determine a content to be a Json feed literal
        /// </summary>
        /// <param name="payload">payload content</param>
        /// <returns>true/false</returns>
        public static bool IsJsonFeed(this string payload)
        {
            JObject jo;
            if (payload.TryToJObject(out jo))
            {
                var inner = jo.ReachInnerToken();
                return (inner != null && inner.Type == JTokenType.Array);
            }

            return false;
        }

        /// <summary>
        /// Extension method to determine whether a content is a Json entry literal
        /// </summary>
        /// <param name="payload">payload content</param>
        /// <returns>true/false</returns>
        public static bool IsJsonEntry(this string payload)
        {
            JObject jo;
            if (payload.TryToJObject(out jo))
            {
                var inner = jo.ReachInnerToken();
                if (inner != null && inner.Type == JTokenType.Object)
                {
                    return ((JObject)inner).Count > 1;
                }
            }

            return false;
        }

        /// <summary>
        /// Extension method to determine whether a content is a Json service document literal
        /// </summary>
        /// <param name="payload">payload content</param>
        /// <returns>true/false</returns>
        public static bool IsJsonSvcDoc(this string payload)
        {
            JObject jo;
            if (payload.TryToJObject(out jo))
            {
                var inner = jo.ReachInnerToken();
                if (inner != null && inner.Type == JTokenType.Object)
                {
                    var o = (JObject)inner;
                    var et = (JProperty)o.First;
                    return o.Count == 1 && et.Name.Equals("EntitySets", StringComparison.Ordinal);
                }
            }

            return false;
        }

        /// <summary>
        /// Extension method to determine whether a content is OData error literal in Json format
        /// </summary>
        /// <param name="payload">payload content</param>
        /// <returns>True if it is OData error literal in Json format; false otherwise</returns>
        public static bool IsJsonError(this string payload)
        {
            bool payloadIsError = false;

            JObject jo;
            if (payload.TryToJObject(out jo))
            {
                if (jo != null && jo.Type == JTokenType.Object)
                {
                    var o = (JObject)jo;
                    var et = (JProperty)o.First;
                    payloadIsError = o.Count == 1 && et.Name.Equals("error", StringComparison.Ordinal);
                }
            }

            return payloadIsError;
        }

        /// <summary>
        /// Checks whether payload is OData link message
        /// </summary>
        /// <param name="payload">The payload to be checked</param>
        /// <returns>True if it is OData link message; false otherwise</returns>
        public static bool IsJsonLink(this string payload)
        {
            bool result = false;

            JObject jo;
            if (payload.TryToJObject(out jo))
            {
                if (jo != null && jo.Type == JTokenType.Object)
                {
                    var inner = jo.ReachInnerToken();
                    if (inner.Type == JTokenType.Array)
                    {
                        var e = ((JArray)inner).First();
                        if (e.Type == JTokenType.Object)
                        {
                            result = JsonObjectIsLink((JObject)e);
                        }
                    }
                    else if (inner.Type == JTokenType.Object)
                    {
                        result = JsonObjectIsLink((JObject)inner);
                    }
                }
            }

            return result;
        }

        /// <summary>
        /// Checks whether a JObject instance is OData link message
        /// </summary>
        /// <param name="o">The JObject instance to be checked</param>
        /// <returns>True if it is OData link message; false otherwise</returns>
        private static bool JsonObjectIsLink(JObject o)
        {
            bool result = false;
            if (o.Count == 1)
            {
                var p = (JProperty)o.First;
                result = p.Name.Equals("uri", StringComparison.Ordinal);
            }
            return result;
        }

        /// <summary>
        /// Checks whether payload is OData property
        /// </summary>
        /// <param name="payload">The payload to be checked</param>
        /// <returns>True if it is OData property message</returns>
        public static bool IsJsonProperty(this string payload)
        {
            bool result = false;

            JObject jo;
            if (payload.TryToJObject(out jo))
            {
                if (jo != null && jo.Type == JTokenType.Object)
                {
                    var inner = jo.ReachInnerToken();
                    if (inner.Type == JTokenType.Object)
                    {
                        result = true;
                    }
                    else if (inner.Type == JTokenType.Array)
                    {
                        result = !((JArray)inner).Any(x => x.Type != JTokenType.Object);
                    }
                }
            }

            return result;
        }

        /// <summary>
        /// Extension method to extract category type string value from a JObject object
        /// </summary>
        /// <param name="jo">The payload in json format</param>
        /// <returns>The full string of type property value of __metadata node in payload</returns>
        public static string GetEntityType(this JObject jo)
        {
            return JsonHelper.GetPropertyOfChild(jo, "__metadata", "type");           
        }

        /// <summary>
        /// Gets full path of entity set.
        /// </summary>
        /// <param name="jo">The payload in json format</param>
        /// <returns>The full path to entity set if one can be found; null otherwise</returns>
        public static string GetEntitySetFullPath(this JObject jo)
        {
            return JsonHelper.GetPropertyOfChild(jo, "__metadata", "uri");
        }

        /// <summary>
        /// Gets property string value of a named child of the Json object.
        /// </summary>
        /// <param name="jo">The Json object</param>
        /// <param name="child">Name of the child object</param>
        /// <param name="property">Name of the property</param>
        /// <returns>Value as string literal of the property</returns>
        public static string GetPropertyOfChild(this JObject jo, string child, string property)
        {
            string value = null;

            if (jo != null)
            {
                var inner = jo.ReachInnerToken();
                if (inner != null)
                {
                    switch (inner.Type)
                    {
                        case JTokenType.Object:
                            value = ((JObject)inner).GetPropertyOfElement(child, property);
                            break;
                        case JTokenType.Array:
                            {
                                var array = (JArray)inner;
                                if (array.Count > 0)
                                {
                                    if (array[0] is JObject)
                                    {
                                        value = ((JObject)array[0]).GetPropertyOfElement(child, property);
                                    }
                                }
                            }

                            break;
                    }
                }
            }

            return value;
        }

        /// <summary>
        /// Extension method to indicate whether a payload Json object is about media link entry
        /// </summary>
        /// <param name="jo">Json object representing payload content</param>
        /// <returns>True for a media link entry; otherwise false</returns>
        public static bool IsMediaLinkEntry(this JObject jo)
        {
            if (jo == null)
            {
                throw new ArgumentNullException("jo");
            }

            var inner = jo.ReachInnerToken();
            if (inner != null && inner.Type == JTokenType.Object) 
            {
                JObject response = (JObject)inner;

                var ps = from p in response.Children<JProperty>()
                         where p.Name.Equals("__metadata", StringComparison.Ordinal)
                         select p;

                if (ps.Any())
                {
                    var meta = ps.First();
                    var et = from e in meta.Value.Children<JProperty>()
                             where e.Name.Equals("content_type", StringComparison.Ordinal)
                             select e;
                    return et.Any();
                }
            }

            return false;
        }

        /// <summary>
        /// Extension method to properly format json payload
        /// </summary>
        /// <param name="jo">JObject object whose payload is to be formatted</param>
        /// <returns>well-formatted payload</returns>
        public static string FineFormat(this JObject jo)
        {
            string result = null; 

            if (jo != null)
            {
                result = jo.ToString();
            }

            return result;
        }

        /// <summary>
        /// Gets property value of named element (direct child) of Json object.
        /// </summary>
        /// <param name="jo">Json object</param>
        /// <param name="element">Name of element of Json object</param>
        /// <param name="propertyName">Name of the property</param>
        /// <returns>Literal string of the found property value of element if one can be found; null otherwise</returns>
        public static string GetPropertyOfElement(this JObject jo, string element, string propertyName)
        {
            string propertyValue = null;

            if (jo != null)
            {
                var items = from p in jo.Children<JProperty>()
                         where p.Name.Equals(element, StringComparison.Ordinal)
                         select p;

                var item = items.FirstOrDefault();
                if (item != null)
                {
                    var itemValue = item.Value;
                    if (itemValue != null)
                    {
                        var properties = from e in itemValue.Children<JProperty>()
                                 where e.Name.Equals(propertyName, StringComparison.Ordinal)
                                 select e;
                        if (properties.Count() > 0)
                        {
                            var typeVal = properties.First().Value;
                            if (typeVal.Type == JTokenType.String)
                            {
                                propertyValue = ((JValue)typeVal).Value.ToString();
                            }
                        }
                    }
                }
            }

            return propertyValue;
        }

        /// <summary>
        /// Gets projection of properties of the specified entity type from payload
        /// </summary>
        /// <param name="payload">The payload content</param>
        /// <param name="metadataDocument">The metadata document</param>
        /// <param name="shortEntityType">The short name of entity type</param>
        /// <returns>Collection of projected properties</returns>
        public static IEnumerable<string> GetProjectedPropertiesFromFeedOrEntry(string payload, string metadataDocument, string shortEntityType)
        {
            IEnumerable<string> projectedProperties = null;

            JObject jo;
            if (payload.TryToJObject(out jo))
            {
                var inner = jo.ReachInnerToken();
                if (inner != null)
                {
                    JToken node = null;

                    if (inner.Type == JTokenType.Array)
                    {
                        // a feed; needs to go further to get one subelement
                        JArray array = (JArray)inner;
                        node = array.First;
                    }
                    else
                    {
                        node = inner;
                    }

                    if (node != null && node.Type == JTokenType.Object)
                    {
                        var ps = from JProperty n in node.Children<JProperty>()
                                 where !n.Name.Equals("__metadata")
                                 select n.Name;
                        if (ps.Any())
                        {
                            int count = ps.Count();
                            int propertiesInMetada = XmlHelper.GetCountOfProperties(shortEntityType, metadataDocument);
                            if (count < propertiesInMetada)
                            {
                                // navigation properties may be included in case of no-projection
                                projectedProperties = ps;
                            }
                        }
                    }
                }
            }

            return projectedProperties;
        }

        /// <summary>
        /// Extension method to get the real Json payload object by stripping off wrapper of "d", and "results"(if it exists) 
        /// </summary>
        /// <param name="jo">JObject which reprensents whole json payload</param>
        /// <returns>JToken inside the trivial wrapper</returns>
        public static JToken ReachInnerToken(this JObject jo)
        {
            if (jo == null)
            {
                return null;
            }

            if (jo.Count == 1)
            {
                var d = (JProperty)jo.First;
                if (d.Name.Equals("d", StringComparison.Ordinal))
                {
                    var sub = d.Value;

                    // strip off v2.0 wrapper of "results"
                    if (sub.Type == JTokenType.Object)
                    {
                        var o = (JObject)sub;
                        if (o.Count == 1)
                        {
                            var r = (JProperty)o.First;
                            if (r.Name.Equals("results", StringComparison.Ordinal))
                            {
                                sub = r.Value;
                            }
                        }
                        else if (o.Count > 1)
                        {
                            // may have "__next:..."
                            foreach (var r in o.Children<JProperty>())
                            {
                                if (r.Name.Equals("results", StringComparison.Ordinal))
                                {
                                    sub = r.Value;
                                    break;
                                }
                            }
                        }
                    }

                    return sub;
                }
            }

            return jo;
        }

        /// <summary>
        /// Gets list of feeds from a service document in JSON format
        /// </summary>
        /// <param name="serviceDocument">The content of service document</param>
        /// <returns>The returned list of feeds exposed by the OData service</returns>
        public static IEnumerable<string> GetFeeds(string serviceDocument)
        {
            if (!serviceDocument.IsJsonSvcDoc())
            {
                throw new ArgumentException(Resource.NotServiceDocument, "serviceDocument");
            }

            IEnumerable<string> feeds = null;
            JObject jo;
            if (serviceDocument.TryToJObject(out jo))
            {
                var inner = jo.ReachInnerToken();
                if (inner != null && inner.Type == JTokenType.Object)
                {
                    var o = (JObject)inner;
                    var et = (JProperty)o.First;    // must be the EntitySets element
                    if (et != null && et.Value != null)
                    {
                        if (et.Value.Type == JTokenType.Array)
                        {
                            feeds = from feed in (JArray)et.Value
                                    select feed.Value<string>();
                        }
                    }
                }
            }

            return feeds;
        }

        /// <summary>
        /// Gets collection of entries from the feed resource in JSON format
        /// </summary>
        /// <param name="feed">The feed resource content</param>
        /// <returns>The entries included in the feed content</returns>
        public static IEnumerable<string> GetEntries(string feed)
        {
            if (!feed.IsJsonFeed())
            {
                throw new ArgumentException(Resource.NotFeed, "feed");
            }

            IEnumerable<string> entries = null;
            JObject jo;
            feed.TryToJObject(out jo);
            var inner = jo.ReachInnerToken();
            if (inner != null)
            {
                if (inner.Type == JTokenType.Array)
                {
                    entries = from JObject a in (JArray)inner
                              select a.GetPropertyOfElement("__metadata", "uri");
                }
            }

            return entries;
        }

        /// <summary>
        /// Get collection of links from the entry in JSON format
        /// </summary>
        /// <param name="entry">The entry resource content</param>
        /// <returns>The links included in the entry content</returns>
        public static IEnumerable<JProperty> GetLinks(string entry)
        {
            if (!entry.IsJsonEntry())
            {
                throw new ArgumentException("The payload is not an entry");
            }

            IEnumerable<JProperty> links = null;
            JObject jo;
            if (entry.TryToJObject(out jo))
            {
                var inner = jo.ReachInnerToken();
                if (inner != null)
                {
                    if (inner.Type == JTokenType.Object)
                    {
                        links = from p in ((JObject)inner).Properties()
                                where p.Value.Type == JTokenType.Object && ((JObject)p.Value).Property("__deferred") != null
                                select p;
                    }
                }
            }

            return links;
        }
    }
}
